Skip to content

chatdlg: replace QTextBrowser with QListView for screen reader accessibility#3748

Closed
mcfnord wants to merge 41 commits into
jamulussoftware:mainfrom
mcfnord:chatdlg-listview-accessibility
Closed

chatdlg: replace QTextBrowser with QListView for screen reader accessibility#3748
mcfnord wants to merge 41 commits into
jamulussoftware:mainfrom
mcfnord:chatdlg-listview-accessibility

Conversation

@mcfnord

@mcfnord mcfnord commented Jun 20, 2026

Copy link
Copy Markdown
Contributor

Replaces `QTextBrowser` with `QListView` + `QStyledItemDelegate` in the chat dialog so that each message is a discrete item in the OS accessibility tree.

With `QTextBrowser`, VoiceOver (and other screen readers that consume `QAccessible`) sees the entire chat history as a single text block. With `QListView`, each appended message becomes a `QAccessible::ListItem`, letting the user arrow-key through individual messages.

`ChatDelegate` renders each item as HTML via `QTextDocument`, preserving the existing rich-text formatting. Link clicks are detected in `eventFilter` by installing a filter on the viewport (mouse events in `QListView` go to the viewport child widget, not the view itself) and using `QTextDocument::documentLayout()->anchorAt()` to hit-test the click position. Hovering over a link shows a pointing-hand cursor. Clicking routes through the existing `OnAnchorClicked` confirmation dialog. Right-click or Ctrl+C copies the selected message's plain text to the clipboard.

Link clicks have been confirmed working on Windows, perhaps on Linux. Does VoiceOver read individual messages as expected on macOS?

Fixes an issue?

Addresses #3613.

Does this change need documentation? What needs to be documented and how?

No documentation changes needed.

Status of this Pull Request

Builds and runs. Not tested on macOS. does VoiceOver navigate individual messages?

I don't see pointer cursor on hover; probably worth investigating.

Checklist

  • I've verified that this Pull Request follows the general code principles
  • I tested my code and it does what I want... but does it do things I don't want?
  • My code follows the style guide
  • I waited some time after this Pull Request was opened and all GitHub checks completed without errors.

@pljones

pljones commented Jun 20, 2026

Copy link
Copy Markdown
Collaborator

Thanks - we've been looking to get more of the assistive side of Jamulus fixed.

Could you also provide screenshots (ideally with some rich HTML text - and welcome messages) and a comparative walk through of use with the existing interface - not just for screen-reader use but generally.

-- Peter

@mcfnord

mcfnord commented Jun 20, 2026

Copy link
Copy Markdown
Contributor Author

I found a problem with this approach (links not clickable) but Claude says it's fixable. I agree that we'd need to assure any visual changes are unimportant. My hope is that the only useability difference is arguably an improvement: you can copy the whole line rather than pieces of it, but no copy of multiple lines anymore. This is a more common pattern in tools like Slack.

Comment thread src/chatdlg.cpp
// set a placeholder text to make sure where to type the message in (#384)
edtLocalInputText->setPlaceholderText ( tr ( "Type a message here" ) );

// Set up the list model and delegate for accessible per-message rows ------

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
// Set up the list model and delegate for accessible per-message rows ------
// Set up the list model and delegate for accessible per-message rows

Check for AI artifacts like this.

@ann0see ann0see left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Tested locally on linux and there is a slight difference. I'd be interested what happens if you send broken html form the server side (e.g. a broken welcome message).

jrd and others added 2 commits June 20, 2026 19:51
Each chat message is now a discrete QListView row rendered by a
QStyledItemDelegate, exposing it to the OS accessibility tree as a
QAccessible::ListItem. VoiceOver can arrow-key through individual
messages instead of treating the whole history as one text block.

Right-click or Ctrl+C copies the selected message's plain text to the
clipboard. Link clicks are detected via the event filter and routed to
the existing confirmation dialog.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Mouse events in QListView go to the viewport child widget, not the
view itself. The event filter was installed only on the view, so
MouseButtonPress was never caught. Install on viewport() too and
check obj == viewport() for mouse events.

Also adds MouseMove handling to show PointingHandCursor over links.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@mcfnord mcfnord force-pushed the chatdlg-listview-accessibility branch from 2766b96 to dde15e4 Compare June 20, 2026 19:52
@mcfnord

mcfnord commented Jun 20, 2026

Copy link
Copy Markdown
Contributor Author

Tested locally on linux and there is a slight difference. I'd be interested what happens if you send broken html form the server side (e.g. a broken welcome message).

I want a Mac user to test with VoiceOver!

@dingodoppelt

Copy link
Copy Markdown
Member

Do @chigkim or @OnjLouis have anything to add to this maybe?

@ann0see

ann0see commented Jun 21, 2026

Copy link
Copy Markdown
Member

I tried it on macOS but could not succeed to tab inside the chat message. It also did not speak once i clicked onto the message. so it didn't fix it unfortunately.

I think that we probably need to resructure the UI anyway. It should probably be restructured such that it works like modern messengers.

softins and others added 20 commits June 22, 2026 19:18
Currently translated at 15.6% (109 of 696 strings)

Translation: Jamulus/Jamulus app
Translate-URL: https://hosted.weblate.org/projects/jamulus/jamulus-app/ru/
Currently translated at 100.0% (696 of 696 strings)

Translation: Jamulus/Jamulus app
Translate-URL: https://hosted.weblate.org/projects/jamulus/jamulus-app/pt_BR/
Currently translated at 100.0% (696 of 696 strings)

Translation: Jamulus/Jamulus app
Translate-URL: https://hosted.weblate.org/projects/jamulus/jamulus-app/pt_BR/
Currently translated at 100.0% (696 of 696 strings)

Translation: Jamulus/Jamulus app
Translate-URL: https://hosted.weblate.org/projects/jamulus/jamulus-app/pt_BR/
Currently translated at 100.0% (696 of 696 strings)

Translation: Jamulus/Jamulus app
Translate-URL: https://hosted.weblate.org/projects/jamulus/jamulus-app/pt_BR/
Currently translated at 100.0% (696 of 696 strings)

Translation: Jamulus/Jamulus app
Translate-URL: https://hosted.weblate.org/projects/jamulus/jamulus-app/pt_BR/
Currently translated at 100.0% (696 of 696 strings)

Translation: Jamulus/Jamulus app
Translate-URL: https://hosted.weblate.org/projects/jamulus/jamulus-app/pt_BR/
Currently translated at 100.0% (696 of 696 strings)

Translation: Jamulus/Jamulus app
Translate-URL: https://hosted.weblate.org/projects/jamulus/jamulus-app/pt_BR/
Satisfies compiler warnings on macOS
ann0see and others added 19 commits June 22, 2026 19:18
Co-authored-by: ann0see <20726856+ann0see@users.noreply.github.com>
For geo redundancy, latency and since Any Genre 3 is not heavily used,
we introduce a directory in Asia. See
jamulussoftware#3692
The default constructor leaves a member uninitialised, and is not needed.
jamulus.app has a better future than jamulus.io due to political
reasons. Thus, the infrastructure was already now moved to jamulus.app
…er events

Two issues prevented VoiceOver from speaking on macOS:

1. Qt::DisplayRole stored raw HTML; VoiceOver read the string verbatim
   (angle-bracket markup), yielding silence or garbled output. Fix: strip
   HTML via QTextDocument::toPlainText() and store the result as
   Qt::AccessibleTextRole on each item — the role QAccessible queries for
   the item's accessible name.

2. The previous QAccessibleValueChangeEvent fired before HTML processing
   and targeted the whole widget with unprocessed text. Replace with
   QAccessibleTableModelChangeEvent::RowsInserted (correct signal for a
   new list row) followed by QAccessibleValueChangeEvent carrying the
   plain text — this drives VoiceOver's live-region-style announcement
   of incoming messages on macOS.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@mcfnord

mcfnord commented Jun 22, 2026

Copy link
Copy Markdown
Contributor Author

Updated with two fixes targeting the macOS VoiceOver silence:

Fix 1 — Qt::AccessibleTextRole (root cause of "didn't speak")

Qt::DisplayRole stores the raw HTML string. VoiceOver reads this role directly — it sees angle-bracket markup and either reads garbled text or says nothing. The fix: after HTML processing, extract plain text with QTextDocument::toPlainText() and store it as Qt::AccessibleTextRole on each item. VoiceOver reads AccessibleTextRole first when naming a list item, so it now gets clean text when the user navigates to a row.

Fix 2 — QAccessibleTableModelChangeEvent::RowsInserted + corrected QAccessibleValueChangeEvent (live-region-style announcement)

The previous QAccessibleValueChangeEvent fired before HTML processing and before the row was even appended — it carried unprocessed text and referenced the whole widget at the wrong moment. Replaced with:

  1. QAccessibleTableModelChangeEvent::RowsInserted targeting the exact row — tells the AT stack a new list item exists.
  2. QAccessibleValueChangeEvent with the plain text, fired after appendRow — this is what drives VoiceOver to announce an incoming message without requiring the user to navigate to it.

Whether (2) produces true live-region auto-announcement on macOS depends on VoiceOver's policy for list widgets, but it's the correct signal sequence; the previous approach was definitely wrong.

@mcfnord

mcfnord commented Jun 22, 2026

Copy link
Copy Markdown
Contributor Author

this conflict thing seems so extra... i think i'll make a new PR

@mcfnord mcfnord closed this Jun 22, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

8 participants